/*
 * Copyright (c) 2023 Cshoo Org. All Rights Reserved.
 */

package org.cshoo.tattoo.gate.config;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.cshoo.tattoo.gate.entity.User;
import org.cshoo.tattoo.gate.service.AuthorityService;
import org.cshoo.tattoo.gate.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import javax.crypto.SecretKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * @author 杨翼
 * @since 1.0
 */
@Slf4j
@Component
public class MyReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Value("${jwt.token.sign.key}")
    private String tokenSignKey;

    @Resource
    private AuthorityService authorityService;

    @SuppressWarnings("unchecked")
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
        ServerWebExchange exchange = authorizationContext.getExchange();
        ServerHttpRequest request = exchange.getRequest();
        String method = request.getMethodValue();
        String path = request.getURI().getPath();

        if (authorityService.checkDenyScope(method, path)) {
            return Mono.justOrEmpty(new AuthorizationDecision(false));
        }

        if (authorityService.checkAnonymousScope(method, path)) {
            return Mono.justOrEmpty(new AuthorizationDecision(true));
        }

        String token = null;
        String authorizationHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (Objects.nonNull(authorizationHeader) && authorizationHeader.startsWith("Bearer ")) {
            token = authorizationHeader.replaceFirst("Bearer ", "");
        }

        log.debug("token:{}", token);
        if (StringUtils.isBlank(token)) {
            return Mono.justOrEmpty(new AuthorizationDecision(false));
        }
        byte[] keyBytes = tokenSignKey.getBytes();
        SecretKey key = Keys.hmacShaKeyFor(keyBytes);

        try {
            Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            String subject = claimsJws.getBody().getSubject();
            if (StringUtils.isBlank(subject)) {
                return Mono.justOrEmpty(new AuthorizationDecision(false));
            }
            log.debug(subject);

            User user = new Gson().fromJson(subject, User.class);
            request.mutate().header("user", subject);
            String username = user.getUsername();

            if (username.equals(UserService.ADMIN_USERNAME) || authorityService.checkEveryoneScope(method, path)) {
                return Mono.justOrEmpty(new AuthorizationDecision(true));
            }

            List<String> authorities = null;
            Object jwsAuthorities = claimsJws.getHeader().get("authorities");
            if (jwsAuthorities instanceof ArrayList) {
                authorities = (ArrayList<String>) jwsAuthorities;
            }

            if (CollectionUtils.isEmpty(authorities)) {
                return Mono.justOrEmpty(new AuthorizationDecision(false));
            }

            for (String authority : authorities) {
                String[] authorityArr = authority.split("\\|");
                String authorityMethod = authorityArr[0];
                String authorityUri = authorityArr[1];
                if (StringUtils.equalsIgnoreCase(authorityMethod, method) && antPathMatcher.match(authorityUri, path)) {
                    log.debug("authority matched,username:{},authority:{},path:{}", username, authority, path);
                    return Mono.justOrEmpty(new AuthorizationDecision(true));
                }
            }
        } catch (JsonSyntaxException e) {
            log.info("invalid JWT token");
            log.error(e.getMessage(), e);
        } catch (ExpiredJwtException e) {
            log.info("JWT expired");
            log.error(e.getMessage(), e);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }

        return Mono.justOrEmpty(new AuthorizationDecision(false));
    }

}
